using System;
using System.Collections.Generic;
using System.Drawing;
using System.Reflection;
using System.Text;

using PerlinNoise.NoiseGenerator;
using PerlinNoise.Interpolator;
using PerlinNoise.Smoother;

namespace PerlinNoise
{
    public class SimplePerlinNoise : AbstractPerlinNoise
    {
        private static readonly Dictionary<uint, PerlinNoiseDimensionPack> DIMENSION_PACKS;
        protected static readonly uint SMOOTH_POINTS_NUMBER_1D;
        protected static readonly int SMOOTH_POINT_START_INDEX;
        protected static readonly int SMOOTH_POINT_END_INDEX;
        protected static readonly double[] SMOOTH_WAGES_1D;
        protected static readonly double[] SMOOTH_WAGES_2D;
        protected static readonly double[] SMOOTH_WAGES_3D;
        protected static readonly double[] SMOOTH_WAGES_4D;

        protected readonly AbstractNoiseGenerator noiseGenerator;
        protected readonly AbstractInterpolator interpolator;
        protected readonly AbstractSmoother smoother;

        static SimplePerlinNoise()
        {
            SMOOTH_POINTS_NUMBER_1D = 3;
            SMOOTH_POINT_START_INDEX = -(int)(SMOOTH_POINTS_NUMBER_1D / 2);
            SMOOTH_POINT_END_INDEX = (int)(SMOOTH_POINTS_NUMBER_1D / 2);
            SMOOTH_WAGES_1D = new double[] { 0.25d, 0.5d, 0.25d };
            SMOOTH_WAGES_2D = new double[] { 
                0.0625d, 0.125d, 0.0625d,
                0.125d, 0.25d, 0.125d,
                0.0625d, 0.125d, 0.0625d 
            };
            SMOOTH_WAGES_3D = null; // TODO: Initialize properly
            SMOOTH_WAGES_4D = null; // TODO: Initialize properly

            // Initialize dimenion packs
            DIMENSION_PACKS = new Dictionary<uint, PerlinNoiseDimensionPack>();
            DIMENSION_PACKS.Add(1, new PerlinNoiseDimensionPack(SMOOTH_WAGES_1D, typeof(AbstractNoiseGenerator).GetMethod("noise1D")));
            DIMENSION_PACKS.Add(2, new PerlinNoiseDimensionPack(SMOOTH_WAGES_2D, typeof(AbstractNoiseGenerator).GetMethod("noise2D")));
            DIMENSION_PACKS.Add(3, new PerlinNoiseDimensionPack(SMOOTH_WAGES_3D, typeof(AbstractNoiseGenerator).GetMethod("noise3D")));
            DIMENSION_PACKS.Add(4, new PerlinNoiseDimensionPack(SMOOTH_WAGES_4D, typeof(AbstractNoiseGenerator).GetMethod("noise4D")));
        }

        public SimplePerlinNoise(AbstractNoiseGenerator _noiseGenerator, AbstractInterpolator _interpolator, AbstractSmoother _smoother)
        {
            this.noiseGenerator = _noiseGenerator;
            this.interpolator = _interpolator;
            this.smoother = _smoother;
        }

        public override double perlinNoise1D(double x)
        {
            int truncX = (int)Math.Floor(x);
            double fracX = x - truncX;

            uint pointsNumber; 
            int startPointIndex;
            if (this.interpolator != null) {
                pointsNumber = this.interpolator.PointsNumber;
                startPointIndex = this.interpolator.StartPointIndex;
            } else {
                pointsNumber = 1;
                startPointIndex = 0;
            }

            double[] smoothedValues = new double[pointsNumber];
            double[] wages = new double[] { 0.25d, 0.5d, 0.25d };

            for (uint i = 0; i < pointsNumber; ++i) {
                int currentX = truncX + startPointIndex + (int)i;
                
                if (this.smoother != null) {
                    double[] values = new double[SMOOTH_POINTS_NUMBER_1D];
                    int startIndex = -(int)SMOOTH_POINTS_NUMBER_1D / 2;
                    for (uint j = 0; j < SMOOTH_POINTS_NUMBER_1D; ++j) {
                        values[j] = this.noiseGenerator.noise1D(currentX + (int)j + startIndex);
                    }

                    smoothedValues[i] = this.smoother.smooth(values, SMOOTH_WAGES_1D);
                } else {
                    smoothedValues[i] = this.noiseGenerator.noise1D(currentX);
                }
            }

            return this.interpolator != null ? this.interpolator.interpolate(smoothedValues, fracX) : smoothedValues[0];
        }

        public override double perlinNoise2D(double x, double y)
        {
            int truncX = (int)Math.Floor(x);
            double fracX = x - truncX;

            int truncY = (int)Math.Floor(y);
            double fracY = y - truncY;

            uint pointsNumber1D;
            uint pointsNumber;
            int startPointIndex;
            if (this.interpolator != null) {
                pointsNumber1D = this.interpolator.PointsNumber;
                pointsNumber = this.interpolator.PointsNumber * this.interpolator.PointsNumber;
                startPointIndex = this.interpolator.StartPointIndex;
            } else {
                pointsNumber1D = 1;
                pointsNumber = 1;
                startPointIndex = 0;
            }

            // Calculate smooth points number
            uint smoothPointsNumber = SMOOTH_POINTS_NUMBER_1D * SMOOTH_POINTS_NUMBER_1D;

            double[] smoothedValues = new double[pointsNumber];
            double[] smoothValues = new double[smoothPointsNumber];

            int currentCoordX = truncX + startPointIndex;
            int currentCoordY = truncY + startPointIndex;
            int maxCurrentCoordX = truncX + startPointIndex + (int)pointsNumber1D;

            int smoothPointCoordX;
            int smoothPointCoordY;
            int maxSmoothPointCoordX;
            for (int i = 0; i < pointsNumber; ++i) {
                if (this.smoother != null) {
                    smoothPointCoordX = currentCoordX + SMOOTH_POINT_START_INDEX;
                    smoothPointCoordY = currentCoordY + SMOOTH_POINT_START_INDEX;
                    maxSmoothPointCoordX = currentCoordX + SMOOTH_POINT_END_INDEX;

                    for (int j = 0; j < smoothPointsNumber; ++j) {
                        smoothValues[j] = this.noiseGenerator.noise2D(smoothPointCoordX, smoothPointCoordY);

                        ++smoothPointCoordX;
                        if (smoothPointCoordX > maxSmoothPointCoordX) {
                            smoothPointCoordX = currentCoordX + SMOOTH_POINT_START_INDEX;
                            ++smoothPointCoordY;
                        }
                    }

                    smoothedValues[i] = this.smoother.smooth(smoothValues, SMOOTH_WAGES_2D);
                } else {
                    smoothedValues[i] = this.noiseGenerator.noise2D(currentCoordX, currentCoordY);
                }

                ++currentCoordX;
                if (currentCoordX >= maxCurrentCoordX) {
                    currentCoordX = truncX + startPointIndex;
                    ++currentCoordY;
                }
            }

            if (this.interpolator != null) {
                uint interpolatesNumber = pointsNumber / pointsNumber1D;
                double[] localSmoothedValues = new double[pointsNumber1D];
                for (uint interpolateIndex = 0; interpolateIndex < interpolatesNumber; ++interpolateIndex) {
                    Array.Copy(smoothedValues, interpolateIndex * pointsNumber1D, localSmoothedValues, 0, pointsNumber1D);
                    smoothedValues[interpolateIndex] = this.interpolator.interpolate(localSmoothedValues, fracX);
                }

                Array.Copy(smoothedValues, 0, localSmoothedValues, 0, pointsNumber1D);
                smoothedValues[0] = this.interpolator.interpolate(localSmoothedValues, fracY);
            }

            return smoothedValues[0];
        }

        public override double perlinNoise3D(double x, double y, double z)
        {
            int truncX = (int)Math.Floor(x);
            double fracX = x - (int)x;

            int truncY = (int)Math.Floor(y);
            double fracY = y - (int)y;

            int truncZ = (int)Math.Floor(z);
            double fracZ = z - (int)z;
    
            uint pointsNumber1D;
            uint pointsNumber;
            int startPointIndex;
            if (this.interpolator != null) {
                pointsNumber1D = this.interpolator.PointsNumber;
                pointsNumber = this.interpolator.PointsNumber * this.interpolator.PointsNumber * this.interpolator.PointsNumber;
                startPointIndex = this.interpolator.StartPointIndex;
            } else {
                pointsNumber1D = 1;
                pointsNumber = 1;
                startPointIndex = 0;
            }

            // Calculate smooth points number
            uint smoothPointsNumber = SMOOTH_POINTS_NUMBER_1D * SMOOTH_POINTS_NUMBER_1D * SMOOTH_POINTS_NUMBER_1D;

            double[] smoothedValues = new double[pointsNumber];
            double[] smoothValues = new double[smoothPointsNumber];

            int currentCoordX = truncX + startPointIndex;
            int currentCoordY = truncY + startPointIndex;
            int currentCoordZ = truncZ + startPointIndex;
            int maxCurrentCoordX = truncX + startPointIndex + (int)pointsNumber1D;
            int maxCurrentCoordY = truncY + startPointIndex + (int)pointsNumber1D;

            int smoothPointCoordX;
            int smoothPointCoordY;
            int smoothPointCoordZ;
            int maxSmoothPointCoordX;
            int maxSmoothPointCoordY;
            for (int i = 0; i < pointsNumber; ++i) {
                if (this.smoother != null) {
                    smoothPointCoordX = currentCoordX + SMOOTH_POINT_START_INDEX;
                    smoothPointCoordY = currentCoordY + SMOOTH_POINT_START_INDEX;
                    smoothPointCoordZ = currentCoordZ + SMOOTH_POINT_START_INDEX;
                    maxSmoothPointCoordX = currentCoordX + SMOOTH_POINT_END_INDEX;
                    maxSmoothPointCoordY = currentCoordY + SMOOTH_POINT_END_INDEX;

                    for (int j = 0; j < smoothPointsNumber; ++j) {
                        smoothValues[j] = this.noiseGenerator.noise3D(smoothPointCoordX, smoothPointCoordY, smoothPointCoordZ);

                        ++smoothPointCoordX;
                        if (smoothPointCoordX > maxSmoothPointCoordX) {
                            smoothPointCoordX = currentCoordX + SMOOTH_POINT_START_INDEX;
                            ++smoothPointCoordY;
                        }
                    }

                    smoothedValues[i] = this.smoother.smooth(smoothValues, SMOOTH_WAGES_3D);
                } else {
                    smoothedValues[i] = this.noiseGenerator.noise3D(currentCoordX, currentCoordY, currentCoordZ);
                }

                ++currentCoordX;
                if (currentCoordX >= maxCurrentCoordX) {
                    currentCoordX = truncX + startPointIndex;
                    ++currentCoordY;

                    if (currentCoordY >= maxCurrentCoordY) {
                        currentCoordY = truncY + startPointIndex;
                        ++currentCoordZ;
                    }
                }
            }

            if (this.interpolator != null) {
                uint interpolatesNumber = pointsNumber / pointsNumber1D;
                double[] localSmoothedValues = new double[pointsNumber1D];
                for (uint interpolateIndex = 0; interpolateIndex < interpolatesNumber; ++interpolateIndex) {
                    Array.Copy(smoothedValues, interpolateIndex * pointsNumber1D, localSmoothedValues, 0, pointsNumber1D);
                    smoothedValues[interpolateIndex] = this.interpolator.interpolate(localSmoothedValues, fracX);
                }

                interpolatesNumber /= pointsNumber1D;
                for (uint interpolateIndex = 0; interpolateIndex < interpolatesNumber; ++interpolateIndex) {
                    Array.Copy(smoothedValues, interpolateIndex * pointsNumber1D, localSmoothedValues, 0, pointsNumber1D);
                    smoothedValues[interpolateIndex] = this.interpolator.interpolate(localSmoothedValues, fracY);
                }

                Array.Copy(smoothedValues, 0, localSmoothedValues, 0, pointsNumber1D);
                smoothedValues[0] = this.interpolator.interpolate(localSmoothedValues, fracZ);
            }

            return smoothedValues[0];
        }

        public override double perlinNoise4D(double x, double y, double z, double t)
        {
            int truncX = (int)Math.Floor(x);
            double fracX = x - (int)x;

            int truncY = (int)Math.Floor(y);
            double fracY = y - (int)y;

            int truncZ = (int)Math.Floor(z);
            double fracZ = z - (int)z;

            int truncT = (int)Math.Floor(t);
            double fracT = t - (int)t;

            uint pointsNumber1D;
            uint pointsNumber;
            int startPointIndex;
            if (this.interpolator != null) {
                pointsNumber1D = this.interpolator.PointsNumber;
                pointsNumber = this.interpolator.PointsNumber * this.interpolator.PointsNumber * this.interpolator.PointsNumber * this.interpolator.PointsNumber;
                startPointIndex = this.interpolator.StartPointIndex;
            } else {
                pointsNumber1D = 1;
                pointsNumber = 1;
                startPointIndex = 0;
            }

            // Calculate smooth points number
            uint smoothPointsNumber = SMOOTH_POINTS_NUMBER_1D * SMOOTH_POINTS_NUMBER_1D * SMOOTH_POINTS_NUMBER_1D * SMOOTH_POINTS_NUMBER_1D;

            double[] smoothedValues = new double[pointsNumber];
            double[] smoothValues = new double[smoothPointsNumber];

            int currentCoordX = truncX + startPointIndex;
            int currentCoordY = truncY + startPointIndex;
            int currentCoordZ = truncZ + startPointIndex;
            int currentCoordT = truncT + startPointIndex;
            int maxCurrentCoordX = truncX + startPointIndex + (int)pointsNumber1D;
            int maxCurrentCoordY = truncY + startPointIndex + (int)pointsNumber1D;
            int maxCurrentCoordZ = truncZ + startPointIndex + (int)pointsNumber1D;

            int smoothPointCoordX;
            int smoothPointCoordY;
            int smoothPointCoordZ;
            int smoothPointCoordT;
            int maxSmoothPointCoordX;
            int maxSmoothPointCoordY;
            int maxSmoothPointCoordZ;
            for (int i = 0; i < pointsNumber; ++i) {
                if (this.smoother != null) {
                    smoothPointCoordX = currentCoordX + SMOOTH_POINT_START_INDEX;
                    smoothPointCoordY = currentCoordY + SMOOTH_POINT_START_INDEX;
                    smoothPointCoordZ = currentCoordZ + SMOOTH_POINT_START_INDEX;
                    smoothPointCoordT = currentCoordT + SMOOTH_POINT_START_INDEX;
                    maxSmoothPointCoordX = currentCoordX + SMOOTH_POINT_END_INDEX;
                    maxSmoothPointCoordY = currentCoordY + SMOOTH_POINT_END_INDEX;
                    maxSmoothPointCoordZ = currentCoordZ + SMOOTH_POINT_END_INDEX;

                    for (int j = 0; j < smoothPointsNumber; ++j) {
                        smoothValues[j] = this.noiseGenerator.noise4D(smoothPointCoordX, smoothPointCoordY, smoothPointCoordZ, smoothPointCoordT);

                        ++smoothPointCoordX;
                        if (smoothPointCoordX > maxSmoothPointCoordX) {
                            smoothPointCoordX = currentCoordX + SMOOTH_POINT_START_INDEX;
                            ++smoothPointCoordY;

                            if (smoothPointCoordY > maxSmoothPointCoordY) {
                                smoothPointCoordY = currentCoordY + SMOOTH_POINT_START_INDEX;
                                ++smoothPointCoordZ;

                                if (smoothPointCoordZ > maxSmoothPointCoordZ) {
                                    smoothPointCoordZ = currentCoordZ + SMOOTH_POINT_START_INDEX;
                                    ++smoothPointCoordT;
                                }
                            }
                        }
                    }

                    smoothedValues[i] = this.smoother.smooth(smoothValues, SMOOTH_WAGES_4D);
                } else {
                    smoothedValues[i] = this.noiseGenerator.noise4D(currentCoordX, currentCoordY, currentCoordZ, currentCoordT);
                }

                ++currentCoordX;
                if (currentCoordX >= maxCurrentCoordX) {
                    currentCoordX = truncX + startPointIndex;
                    ++currentCoordY;

                    if (currentCoordY >= maxCurrentCoordY) {
                        currentCoordY = truncY + startPointIndex;
                        ++currentCoordZ;

                        if (currentCoordZ >= maxCurrentCoordZ) {
                            currentCoordZ = truncZ + startPointIndex;
                            ++currentCoordT;
                        }
                    }
                }
            }

            if (this.interpolator != null) {
                uint interpolatesNumber = pointsNumber / pointsNumber1D;
                double[] localSmoothedValues = new double[pointsNumber1D];
                for (uint interpolateIndex = 0; interpolateIndex < interpolatesNumber; ++interpolateIndex) {
                    Array.Copy(smoothedValues, interpolateIndex * pointsNumber1D, localSmoothedValues, 0, pointsNumber1D);
                    smoothedValues[interpolateIndex] = this.interpolator.interpolate(localSmoothedValues, fracX);
                }

                interpolatesNumber /= pointsNumber1D;
                for (uint interpolateIndex = 0; interpolateIndex < interpolatesNumber; ++interpolateIndex) {
                    Array.Copy(smoothedValues, interpolateIndex * pointsNumber1D, localSmoothedValues, 0, pointsNumber1D);
                    smoothedValues[interpolateIndex] = this.interpolator.interpolate(localSmoothedValues, fracY);
                }

                interpolatesNumber /= pointsNumber1D;
                for (uint interpolateIndex = 0; interpolateIndex < interpolatesNumber; ++interpolateIndex) {
                    Array.Copy(smoothedValues, interpolateIndex * pointsNumber1D, localSmoothedValues, 0, pointsNumber1D);
                    smoothedValues[interpolateIndex] = this.interpolator.interpolate(localSmoothedValues, fracZ);
                }

                Array.Copy(smoothedValues, 0, localSmoothedValues, 0, pointsNumber1D);
                smoothedValues[0] = this.interpolator.interpolate(localSmoothedValues, fracT);
            }

            return smoothedValues[0];
        }

        private double perlinNoiseGeneric(uint dimension, double[] coords)
        {
            PerlinNoiseDimensionPack perlinNoiseDimensionPack = DIMENSION_PACKS[dimension];

            if (perlinNoiseDimensionPack == null) {
                throw new NotImplementedException(String.Format("There's no perlin noise implementation for requested dimension: {0}", dimension));
            }

            // Calculate integral and fractional parts of requested coords
            int[] truncCoords = new int[dimension];
            double[] fracCoords = new double[dimension];
            for (uint i = 0; i < dimension; ++i) {
                truncCoords[i] = (int)Math.Floor(coords[i]);
                fracCoords[i] = coords[i] - (int)coords[i];
            }

            // Calculate points number and index shift needed for interpolation
            uint pointsNumber1D;
            uint pointsNumber;
            int startPointIndex;
            if (this.interpolator != null) {
                pointsNumber1D = this.interpolator.PointsNumber;
                pointsNumber = 1;
                for (uint i = 0; i < dimension; ++i) {
                    pointsNumber *= this.interpolator.PointsNumber;
                }
                startPointIndex = this.interpolator.StartPointIndex;
            } else {
                pointsNumber1D = 1;
                pointsNumber = 1;
                startPointIndex = 0;
            }

            // Calculate smooth points number
            uint smoothPointsNumber = 1;
            for (uint smoothPointsNumberIndex = 0; smoothPointsNumberIndex < dimension; ++smoothPointsNumberIndex) {
                smoothPointsNumber *= SMOOTH_POINTS_NUMBER_1D;
            }

            // Create all arrays now, to avoid creating them in the loop
            double[] smoothedValues = new double[pointsNumber]; // Calculate enough values for interpolator
            int[] currentCoords = new int[dimension]; // Current coords for first smoothed point
            object[] smoothPointCoords = new object[dimension]; // Current coords for smoother point, object[] needed for Invoke
            double[] values = new double[smoothPointsNumber]; // Holds values for smoothing
            int[] indexes = new int[dimension]; // Holds currently processed point indexes relative to currentCoords

            // Calculate current coords for first point
            for (uint currentCoordsIndex = 0; currentCoordsIndex < dimension; ++currentCoordsIndex) {
                currentCoords[currentCoordsIndex] = truncCoords[currentCoordsIndex] + startPointIndex;
            }

            // Calculate smoothed values
            for (uint smoothedPointsIndex = 0; smoothedPointsIndex < pointsNumber; ++smoothedPointsIndex) {
                if (this.smoother != null) {
                    // Calculate coords for first smooth point
                    for (uint smoothPointIndex = 0; smoothPointIndex < dimension; ++smoothPointIndex) {
                        smoothPointCoords[smoothPointIndex] = currentCoords[smoothPointIndex] + SMOOTH_POINT_START_INDEX;
                    }

                    // Calculate values needed for smoothing
                    for (uint smoothPointsNumberIndex = 0; smoothPointsNumberIndex < smoothPointsNumber; ++smoothPointsNumberIndex) {
                        // Calculate value for current smooth point
                        values[smoothPointsNumberIndex] = (double)perlinNoiseDimensionPack.GenerateNoiseMethod.Invoke(this.noiseGenerator,
                            smoothPointCoords);
                        
                        // Move smooth point to one further
                        smoothPointCoords[0] = (int)smoothPointCoords[0] + 1;
                        for (int smoothPointCoordsIndex = 0; smoothPointCoordsIndex < smoothPointCoords.Length; ++smoothPointCoordsIndex) {
                            if ((int)smoothPointCoords[smoothPointCoordsIndex] > (int)currentCoords[smoothPointCoordsIndex] + SMOOTH_POINT_END_INDEX && smoothPointCoordsIndex + 1 < smoothPointCoords.Length) {
                                smoothPointCoords[smoothPointCoordsIndex] = currentCoords[smoothPointCoordsIndex] + SMOOTH_POINT_START_INDEX;
                                smoothPointCoords[smoothPointCoordsIndex + 1] = (int)smoothPointCoords[smoothPointCoordsIndex + 1] + 1;
                            }
                        }
                    }

                    // Smooth values for given point
                    smoothedValues[smoothedPointsIndex] = this.smoother.smooth(values, perlinNoiseDimensionPack.SmoothWages);
                } else {
                    // Calculate coords for first smooth point
                    for (uint smoothPointIndex = 0; smoothPointIndex < dimension; ++smoothPointIndex) {
                        smoothPointCoords[smoothPointIndex] = currentCoords[smoothPointIndex];
                    }

                    // Invoke noise generator without smoothing
                    smoothedValues[smoothedPointsIndex] = (double)perlinNoiseDimensionPack.GenerateNoiseMethod.Invoke(this.noiseGenerator, smoothPointCoords);
                }

                // Move current point to one further
                ++currentCoords[0];
                for (int currentCoordsIndex = 0; currentCoordsIndex < currentCoords.Length; ++currentCoordsIndex) {
                    if (currentCoords[currentCoordsIndex] >= truncCoords[currentCoordsIndex] + startPointIndex + pointsNumber1D && currentCoordsIndex + 1 < currentCoords.Length) {
                        currentCoords[currentCoordsIndex] = truncCoords[currentCoordsIndex] + startPointIndex;
                        ++currentCoords[currentCoordsIndex + 1];
                    }
                }
            }

            // Interpolate values, result will be stored in smootheValues[0]
            if (this.interpolator != null) {
                uint interpolatesNumber = pointsNumber / pointsNumber1D;
                double[] localSmoothedValues = new double[pointsNumber1D];
                for (uint dimensionIndex = 0; dimensionIndex < dimension; ++dimensionIndex) {
                    for (uint interpolateIndex = 0; interpolateIndex < interpolatesNumber; ++interpolateIndex) {
                        Array.Copy(smoothedValues, interpolateIndex * pointsNumber1D, localSmoothedValues, 0, pointsNumber1D);
                        smoothedValues[interpolateIndex] = this.interpolator.interpolate(localSmoothedValues, fracCoords[dimensionIndex]);
                    }
                    interpolatesNumber /= pointsNumber1D;
                }
            }

            return smoothedValues[0];
        }
    }
}
